﻿namespace StyleCop.Extensions
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using StyleCop.CSharp;

    [SourceAnalyzer(typeof(CsParser))]
    public class LokiNamingRules : SourceAnalyzer
    {
        #region Internal Constants

        /// <summary>
        /// The name of the property containing the list of allowable prefixes.
        /// </summary>
        internal const string AllowedPrefixesProperty = "Hungarian";

        #endregion Internal Constants

        #region Public Constructors

        /// <summary>
        /// Initializes a new instance of the NamingRules class.
        /// </summary>
        public LokiNamingRules()
        {
        }

        #endregion Public Constructors


        #region Public Override Methods

        /// <summary>
        /// Checks the case of element names within the given document.
        /// </summary>
        /// <param name="document">The document to check.</param>
        public override void AnalyzeDocument(CodeDocument document)
        {
            Param.RequireNotNull(document, "document");

            CsDocument csdocument = (CsDocument)document;

            if (csdocument.RootElement != null && !csdocument.RootElement.Generated)
            {
                Dictionary<string, string> validPrefixes = this.GetPrefixes(document.Settings);
                this.ProcessElement(csdocument.RootElement, validPrefixes, false);
            }
        }

        /// <inheritdoc />
        public override bool DoAnalysis(CodeDocument document)
        {
            Param.RequireNotNull(document, "document");

            CsDocument csdocument = (CsDocument)document;

            return csdocument.FileHeader == null || !csdocument.FileHeader.UnStyled;
        }

        #endregion Public Override Methods

        #region Private Static Methods


        #endregion Private Static Methods

        #region Private Methods

        /// <summary>
        /// Gets the list of valid prefixes for the given project.
        /// </summary>
        /// <param name="settings">The settings for the document being parsed.</param>
        /// <returns>Returns the list of prefixes.</returns>
        private Dictionary<string, string> GetPrefixes(Settings settings)
        {
            Param.Ignore(settings);

            Dictionary<string, string> validPrefixes = new Dictionary<string, string>();

            if (settings != null)
            {
                // Get the allowed hungarian prefixes from the local settings file.
                CollectionProperty list = this.GetSetting(settings, LokiNamingRules.AllowedPrefixesProperty) as CollectionProperty;
                if (list != null && list.Count > 0)
                {
                    foreach (string value in list)
                    {
                        if (!string.IsNullOrEmpty(value) && !validPrefixes.ContainsKey(value))
                        {
                            validPrefixes.Add(value, value);
                        }
                    }
                }
            }

            return validPrefixes;
        }
        /// <summary>
        /// Processes one element and its children.
        /// </summary>
        /// <param name="element">The element to process.</param>
        /// <param name="validPrefixes">The list of valid prefixes for this element.</param>
        /// <param name="nativeMethods">Indicates whether the element is within a NativeMethods class.</param>
        /// <returns>Returns false if the analyzer should quit.</returns>
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity", Justification = "Minimizing refactoring before release.")]
        private bool ProcessElement(CsElement element, Dictionary<string, string> validPrefixes, bool nativeMethods)
        {
            Param.AssertNotNull(element, "element");
            Param.AssertNotNull(validPrefixes, "validPrefixes");
            Param.Ignore(nativeMethods);

            if (this.Cancel)
            {
                return false;
            }

            if (!element.Generated && element.Declaration != null && element.Declaration.Name != null)
            {
                switch (element.ElementType)
                {
                    case ElementType.Namespace:
                        if (!nativeMethods)
                        {
                            string[] namespaceParts = element.Declaration.Name.Split('.');
                            foreach (string namespacePart in namespaceParts)
                            {
                                this.CheckCase(element, namespacePart, element.LineNumber, true);
                            }
                        }

                        break;

                    case ElementType.Class:
                    case ElementType.Enum:
                    case ElementType.Struct:
                    case ElementType.Property:
                        if (!nativeMethods)
                        {
                            this.CheckSize(element, element.Declaration.Name, element.LineNumber);
                            this.CheckCase(element, element.Declaration.Name, element.LineNumber, true);
                        }

                        break;

                    case ElementType.Event:
                        if (!nativeMethods)
                        {
                            foreach (EventDeclaratorExpression declarator in ((Event)element).Declarators)
                            {
                                this.CheckSize(element, element.Declaration.Name, element.LineNumber);
                                this.CheckCase(element, declarator.Identifier.Text, declarator.LineNumber, true);
                            }
                        }

                        break;
                    case ElementType.Indexer:
                    case ElementType.Delegate:
                    case ElementType.Constructor:
                    case ElementType.Method:
                        if (!nativeMethods &&
                            !element.Declaration.Name.StartsWith("operator", StringComparison.Ordinal) &&
                            element.Declaration.Name != "foreach")
                        {
                            this.CheckSize(element, element.Declaration.Name, element.LineNumber);
                            this.CheckCase(element, element.Declaration.Name, element.LineNumber, true);
                            this.CheckMethodParameters(element, element.Declaration.Name, element.LineNumber);
                        }

                        break;

                    case ElementType.Interface:
                        if (element.Declaration.Name.Length < 1 || element.Declaration.Name[0] != 'I')
                        {
                            this.CheckSize(element, element.Declaration.Name, element.LineNumber);
                            this.AddViolation(element, Rules.InterfaceNamesMustBeginWithI, element.Declaration.Name);
                        }

                        break;

                    case ElementType.Field:
                        if (!nativeMethods)
                        {
                            this.CheckSize(element, element.Declaration.Name, element.LineNumber);
                            this.CheckFieldPrefix(element as Field, validPrefixes);
                        }

                        break;
                    default:
                        break;
                }
            }

            if (!nativeMethods &&
                (element.ElementType == ElementType.Class || element.ElementType == ElementType.Struct) &&
                element.Declaration.Name.EndsWith("NativeMethods", StringComparison.Ordinal))
            {
                nativeMethods = true;
            }

            bool doneAccessor = false;
            foreach (CsElement child in element.ChildElements)
            {
                if ((element.ElementType == ElementType.Indexer && !doneAccessor) || element.ElementType != ElementType.Indexer)
                {
                    if (child.ElementType == ElementType.Accessor)
                    {
                        doneAccessor = true;
                    }

                    if (!this.ProcessElement(child, validPrefixes, nativeMethods))
                    {
                        return false;
                    }
                }
            }

            if (!nativeMethods)
            {
                this.ProcessStatementContainer(element, validPrefixes);
            }

            return true;
        }

        /// <summary>
        /// Processes the given statement container.
        /// </summary>
        /// <param name="element">The statement container element to process.</param>
        /// <param name="validPrefixes">The list of acceptable Hungarian-type prefixes.</param>
        private void ProcessStatementContainer(CsElement element, Dictionary<string, string> validPrefixes)
        {
            Param.AssertNotNull(element, "element");
            Param.AssertNotNull(validPrefixes, "validPrefixes");

            // Check the statement container's variables.
            if (element.Variables != null)
            {
                /*foreach (Variable variable in element.Variables)
                {
                    if (!variable.Generated)
                    {
                        this.CheckMethodVariablePrefix(variable, element, validPrefixes);
                    }
                }*/
            }

            // Check each of the statements under this container.
            foreach (Statement statement in element.ChildStatements)
            {
                this.ProcessStatement(statement, element, validPrefixes);
            }
        }

        /// <summary>
        /// Gets the statement, if any.
        /// </summary>
        /// <param name="codeUnit">The code unit.</param>
        /// <returns>The statement, if any.</returns>
        public static Statement ContainingStatement(ICodeUnit codeUnit)
        {

            return Ancestors(codeUnit)
                .OfType<Statement>()
                .FirstOrDefault();
        }

        /// <summary>
        /// Enumerates the code unit's ancestors.
        /// </summary>
        /// <param name="codeUnit">The code unit.</param>
        /// <returns>The code unit's ancestors.</returns>
        public static IEnumerable<ICodeUnit> Ancestors(ICodeUnit codeUnit)
        {
            var ancestor = (ICodeUnit)codeUnit.Parent;
            while (ancestor != null)
            {
                yield return ancestor;

                ancestor = (ICodeUnit)ancestor.Parent;
            }
        }

        /// <summary>
        /// Gets the <see cref="Variable"/> from the <see cref="VariableDeclaratorExpression"/>.
        /// </summary>
        /// <param name="variableDeclarator">The variable declarator.</param>
        /// <returns>The <see cref="Variable"/>.</returns>
        public static Variable GetVariable(VariableDeclaratorExpression variableDeclarator)
        {
            if (variableDeclarator == null)
            {
                throw new ArgumentNullException("variableDeclarator");
            }

            if (ContainingStatement(variableDeclarator) is CatchStatement)
            {
                return ContainingStatement(variableDeclarator).Variables[variableDeclarator.Identifier.Text];
            }

            var parentCodeUnit = (ICodeUnit)ContainingStatement(variableDeclarator).Parent;
            return parentCodeUnit.Variables[variableDeclarator.Identifier.Text];
        }

        /// <summary>
        /// Processes the given statement.
        /// </summary>
        /// <param name="statement">The statement to process.</param>
        /// <param name="element">The parent element.</param>
        /// <param name="validPrefixes">The list of acceptable Hungarian-type prefixes.</param>
        private void ProcessStatement(Statement statement, CsElement element, Dictionary<string, string> validPrefixes)
        {
            Param.AssertNotNull(statement, "statement");
            Param.AssertNotNull(element, "element");
            Param.AssertNotNull(validPrefixes, "validPrefixes");

            if (statement.StatementType == StatementType.VariableDeclaration)
            {
                foreach (var declarator in ((VariableDeclarationStatement)statement).Declarators)
                {
                    if (GetVariable(declarator) != null)
                    {
                        this.CheckMethodVariablePrefix(GetVariable(declarator), element, validPrefixes);
                    }
                }
            }

            foreach (var tokenNode in element.ElementTokens)
            {
                Field L_Field = element as Field;

                if (L_Field == null || !(L_Field.Const || L_Field.Readonly))
                {
                    if (tokenNode.CsTokenType == CsTokenType.String)
                    {
                        this.AddViolation(element,
                        tokenNode.LineNumber, Rules.CodeMustNotContainHardcodedStrings, element.Name);
                    }
                }
            }

            /* // Check the statement's variables.
             if (statement.Variables != null)
             {
                 foreach (Variable variable in statement.Variables)
                 {
                     this.CheckMethodVariablePrefix(variable, element, validPrefixes);
                 }
             }*/

            // Check the expressions under this statement.
            foreach (Expression expression in statement.ChildExpressions)
            {
                this.ProcessExpression(expression, element, validPrefixes);
            }

            // Check each of the statements under this statement.
            foreach (Statement childStatement in statement.ChildStatements)
            {
                this.ProcessStatement(childStatement, element, validPrefixes);
            }
        }

        /// <summary>
        /// Processes the given expression.
        /// </summary>
        /// <param name="expression">The expression to process.</param>
        /// <param name="element">The parent element.</param>
        /// <param name="validPrefixes">The list of acceptable Hungarian-type prefixes.</param>
        private void ProcessExpression(Expression expression, CsElement element, Dictionary<string, string> validPrefixes)
        {
            Param.AssertNotNull(expression, "expression");
            Param.AssertNotNull(element, "element");
            Param.AssertNotNull(validPrefixes, "validPrefixes");

            // Check the type of the expression.
            if (expression.ExpressionType == ExpressionType.AnonymousMethod)
            {
                AnonymousMethodExpression anonymousMethod = (AnonymousMethodExpression)expression;

                // Check the anonymous method's variables.
                if (anonymousMethod.Variables != null)
                {
                    /*foreach (Variable variable in anonymousMethod.Variables)
                    {
                        this.CheckMethodVariablePrefix(variable, element, validPrefixes);
                    }*/

                    // Check the statements under the anonymous method.
                    foreach (Statement statement in anonymousMethod.ChildStatements)
                    {
                        this.ProcessStatement(statement, element, validPrefixes);
                    }
                }
            }

            // Check the child expressions under this expression.
            foreach (Expression childExpression in expression.ChildExpressions)
            {
                this.ProcessExpression(childExpression, element, validPrefixes);
            }
        }

        /// <summary>
        /// Checks the case of the first character in the given word.
        /// </summary>
        /// <param name="element">The element that the word appears in.</param>
        /// <param name="name">The word to check.</param>
        /// <param name="line">The line that the word appears on.</param>
        /// <param name="upper">True if the character should be upper, false if it should be lower.</param>
        private void CheckCase(CsElement element, string name, int line, bool upper)
        {
            Param.AssertNotNull(element, "element");
            Param.AssertValidString(name, "name");
            Param.AssertGreaterThanZero(line, "line");
            Param.Ignore(upper);

            if (name.Length >= 1)
            {
                char firstLetter = name[0];

                // If the first character is not a letter, then it does not make any sense to check
                // for upper or lower case.
                if (char.IsLetter(firstLetter))
                {
                    if (!char.IsUpper(firstLetter))
                    {
                        this.AddViolation(element, line, Rules.ElementMustBeginWithUpperCaseLetter, element.FriendlyTypeText, name);
                    }
                }
            }
        }

        private void CheckSize(CsElement element, string name, int line)
        {
            string CleanName = RemovePrefix(name);
            if (CleanName.Length < 3 && !String.Equals(CleanName, CleanName.ToUpperInvariant(), StringComparison.Ordinal))
            {
                this.AddViolation(element, line, Rules.ElementNameMustHaveLongName, element.FriendlyTypeText, name);
            }
        }

        private void CheckMethodParameters(CsElement element, string name, int line)
        {
            IList<Parameter> parameters = GetParameters(element);

            if (parameters != null)
            {
                foreach (var item in parameters)
                {
                    if (item.Name == "value" && (element.ElementType == ElementType.Property || element.ElementType == ElementType.Accessor))
                        return;

                    // If argument type is 'EventArgs' allow non-Camel case variable name also.
                    if (!item.Type.Text.ToLower().EndsWith("EventArgs".ToLower()))
                    {
                        if (!item.Name.StartsWith("P_"))
                            AddViolation(element, Rules.MethodParameterPUnderscorePrefix, item.Name);
                        CheckCase(element, RemovePrefix(item.Name), item.LineNumber, true);
                    }
                }
            }
        }

        private IList<Parameter> GetParameters(CsElement element)
        {
            IList<Parameter> parameters = null;

            if (element.ElementType == ElementType.Constructor)
            {
                parameters = ((Constructor)element).Parameters;
            }
            else if (element.ElementType == ElementType.Accessor)
            {
                parameters = ((Accessor)element).Parameters;
            }
            else if (element.ElementType == ElementType.Delegate)
            {
                parameters = ((StyleCop.CSharp.Delegate)element).Parameters;
            }
            else if (element.ElementType == ElementType.Method)
            {
                parameters = ((Method)element).Parameters;
            }
            else if (element.ElementType == ElementType.Indexer)
            {
                parameters = ((Indexer)element).Parameters;
            }
            return parameters;
        }

        /// <summary>
        /// Checks a field for compliance with naming prefix rules.
        /// </summary>
        /// <param name="field">The field element.</param>
        /// <param name="validPrefixes">A list of valid prefixes that should not be considered hungarian.</param>
        private void CheckFieldPrefix(Field field, Dictionary<string, string> validPrefixes)
        {
            Param.AssertNotNull(field, "field");
            Param.Ignore(validPrefixes);
            if (field.Const || field.Readonly)
            {
                if (!string.Equals(field.Declaration.Name.ToUpperInvariant(), field.Declaration.Name, StringComparison.Ordinal))
                {
                    this.AddViolation(
                    field,
                    field.LineNumber,
                    Rules.ConstFieldMustBeUppercase,
                    field.Declaration.Name);
                }
            }
            else if (!field.Declaration.Name.StartsWith("_"))
            {
                if (field.Declaration.Name.Length > 3 && field.Declaration.Name[3] == '_')
                {
                    // prefix
                    string prefix = field.Declaration.Name.SubstringBefore('_');
                    if (!string.Equals(prefix.ToUpperInvariant(), prefix, StringComparison.Ordinal))
                    {
                        this.AddViolation(
                        field,
                        field.LineNumber,
                        Rules.FieldPrefixMustBeUppercase,
                        field.Declaration.Name);
                    }
                }
                else
                {
                    this.AddViolation(
                            field,
                            field.LineNumber,
                            Rules.FieldMustBeginWithUnderscoreOrPrefixed,
                            field.Declaration.Name,
                            field.Declaration.Name.Length > 3 ? field.Declaration.Name[3].ToString() : "N/A");
                }
            }

            CheckCase(field, RemovePrefix(field.Declaration.Name), field.LineNumber, true);
        }

        /// <summary>
        /// Checks the prefix for a variable defined within a method or property.
        /// </summary>
        /// <param name="variable">The variable to check.</param>
        /// <param name="element">The element that contains the variable.</param>
        /// <param name="validPrefixes">A list of valid prefixes that should not be considered hungarian.</param>
        private void CheckMethodVariablePrefix(
            Variable variable, CsElement element, Dictionary<string, string> validPrefixes)
        {
            Param.AssertNotNull(variable, "variable");
            Param.AssertNotNull(element, "element");
            Param.Ignore(validPrefixes);

            var parameters = GetParameters(element);

            if (variable.Name == "value" && (element.ElementType == ElementType.Property || element.ElementType == ElementType.Accessor))
                return;

            if (!variable.Name.StartsWith("L_"))
            {
                if (parameters == null || !parameters.Any(x => x.Name == variable.Name))
                {
                    this.AddViolation(element,
                        variable.Location.LineNumber,
                        Rules.MethodVariableLUnderscorePrefix, variable.Name, element.Name);
                }
            }

            CheckCase(element, RemovePrefix(variable.Name), variable.Location.LineNumber, true);
            CheckSize(element, RemovePrefix(variable.Name), variable.Location.LineNumber);
        }

        private string RemovePrefix(string Name)
        {
            if (Name.IndexOf('_') > 0)
            {
                return Name.SubstringAfter('_');
            }
            else
            {
                return Name;
            }
        }

        #endregion Private Methods
    }
}
